C.S.Burner 🪙 ━►🔥GIT Node
Node / reticulum_mirror / MeshChatX / files / meshchatx / src / frontend / components / map / MiniChat.vue
Displaying Raw • Download
meshchatx/src/frontend/components/map/MiniChat.vue 65ca9ddc823fcc2c8f8653c553b944b85db0875f (65ca9ddc) Text, 8.01 KB
T8b949e<!-- SPDX-License-Identifier: 0BSD -->
Tff7b72<templateTff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col h-64 bg-gray-50 dark:bg-zinc-950 rounded-lg overflow-hidden border border-gray-200 dark:border-zinc-800Ta5d6ff"
Tff7b72>
T8b949e<!-- message list -->
Tff7b72<div Te6edf3ref=Ta5d6ff"Ta5d6ffmessageListTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 overflow-y-auto p-2 space-y-2 scrollbar-thinTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffloadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffflex justify-center py-4Ta5d6ff"Tff7b72>
Tff7b72<v-icon Te6edf3icon=Ta5d6ff"Ta5d6ffmdi-loadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffanimate-spin text-gray-400Ta5d6ff" Te6edf3size=Ta5d6ff"Ta5d6ff20Ta5d6ff"Tff7b72>Tff7b72</v-icon>
Tff7b72</div>
Tff7b72<div Te6edf3v-else-if=Ta5d6ff"Ta5d6ffmessages.length === 0Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6fftext-center py-4 text-xs text-gray-400Ta5d6ff"Tff7b72>No messages yetTff7b72</div>
Tff7b72<div
Te6edf3v-for=Ta5d6ff"Ta5d6ffmsg in messagesTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6ffmsg.hashTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col max-w-[90%]Ta5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ffmsg.is_outbound ? Ta5d6ff'Ta5d6ffml-auto items-endTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffmr-auto items-startTa5d6ff'Ta5d6ff"
Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffpx-2 py-1 rounded-lg text-xs wrap-break-word shadow-xsTa5d6ff"
Te6edf3:class=Ta5d6ff"
Ta5d6ff msg.is_outbound
? Ta5d6ff'Ta5d6ffbg-blue-600 text-whiteTa5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ffbg-white dark:bg-zinc-900 text-gray-800 dark:text-zinc-200Ta5d6ff'
Ta5d6ff Ta5d6ff"
Tff7b72>
T8b949e<!-- Telemetry Header if no content -->
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ff!msg.content && msg.fields?.telemetryTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-1 mb-1 pb-1 border-b border-white/10 opacity-80Ta5d6ff"
Tff7b72>
Tff7b72<v-icon Te6edf3icon=Ta5d6ff"Ta5d6ffmdi-satellite-variantTa5d6ff" Te6edf3size=Ta5d6ff"Ta5d6ff10Ta5d6ff"Tff7b72>Tff7b72</v-icon>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftext-[8px] font-bold uppercase tracking-widerTa5d6ff"
Tff7b72>{{ msg.is_outbound ? "Sent" : "Received" }} TelemetryTff7b72</span
>
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ff!msg.content && msg.fields?.commands?.some((c) => c[Ta5d6ff'Ta5d6ff0x01Ta5d6ff'Ta5d6ff] || c[Ta5d6ff'Ta5d6ff1Ta5d6ff'Ta5d6ff] || c[Ta5d6ff'Ta5d6ff0x1Ta5d6ff'Ta5d6ff])Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-1 mb-1 pb-1 border-b border-white/10 opacity-80Ta5d6ff"
Tff7b72>
Tff7b72<v-icon Te6edf3icon=Ta5d6ff"Ta5d6ffmdi-crosshairs-questionTa5d6ff" Te6edf3size=Ta5d6ff"Ta5d6ff10Ta5d6ff"Tff7b72>Tff7b72</v-icon>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftext-[8px] font-bold uppercase tracking-widerTa5d6ff"Tff7b72>Location RequestTff7b72</span>
Tff7b72</div>
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffmsg.contentTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffleading-normalTa5d6ff"Tff7b72>{{ msg.content }}Tff7b72</div>
T8b949e<!-- Mini Telemetry Data -->
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffmsg.fields?.telemetryTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffmt-1 space-y-1Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffmsg.fields.telemetry.locationTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-1 text-[9px] font-mono opacity-90Ta5d6ff"
Tff7b72>
Tff7b72<v-icon Te6edf3icon=Ta5d6ff"Ta5d6ffmdi-map-markerTa5d6ff" Te6edf3size=Ta5d6ff"Ta5d6ff10Ta5d6ff"Tff7b72>Tff7b72</v-icon>
Tff7b72<span
Tff7b72>{{ msg.fields.telemetry.location.latitude.toFixed(4) }},
{{ msg.fields.telemetry.location.longitude.toFixed(4) }}Tff7b72</span
>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex gap-2 opacity-70 text-[8px]Ta5d6ff"Tff7b72>
Tff7b72<span Te6edf3v-if=Ta5d6ff"Ta5d6ffmsg.fields.telemetry.batteryTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-0.5Ta5d6ff"Tff7b72>
Tff7b72<v-icon Te6edf3icon=Ta5d6ff"Ta5d6ffmdi-batteryTa5d6ff" Te6edf3size=Ta5d6ff"Ta5d6ff8Ta5d6ff"Tff7b72>Tff7b72</v-icon
>{{ msg.fields.telemetry.battery.charge_percent }}%
Tff7b72</span>
Tff7b72<span Te6edf3v-if=Ta5d6ff"Ta5d6ffmsg.fields.telemetry.physical_linkTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-0.5Ta5d6ff"Tff7b72>
Tff7b72<v-icon Te6edf3icon=Ta5d6ff"Ta5d6ffmdi-antennaTa5d6ff" Te6edf3size=Ta5d6ff"Ta5d6ff8Ta5d6ff"Tff7b72>Tff7b72</v-icon>SNR:
{{ msg.fields.telemetry.physical_link.snr }}dB
Tff7b72</span>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-[8px] text-gray-400 mt-0.5Ta5d6ff"Tff7b72>
{{ formatTime(msg.timestamp) }}
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
T8b949e<!-- input -->
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffp-2 bg-white dark:bg-zinc-900 border-t border-gray-200 dark:border-zinc-800Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex gap-1Ta5d6ff"Tff7b72>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffnewMessageTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-md px-2 py-1 text-xs focus:outline-hidden focus:ring-1 focus:ring-blue-500 text-gray-900 dark:text-zinc-100Ta5d6ff"
Te6edf3placeholder=Ta5d6ff"Ta5d6ffType a message...Ta5d6ff"
@keydown.Te6edf3enter=Ta5d6ff"Ta5d6ffsendMessageTa5d6ff"
Tff7b72/>
Tff7b72<button
Te6edf3:disabled=Ta5d6ff"Ta5d6ff!newMessage.trim() || sendingTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffp-1.5 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 dark:disabled:bg-zinc-700 text-white rounded-md transition-colorsTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffsendMessageTa5d6ff"
Tff7b72>
Tff7b72<v-icon
Te6edf3:icon=Ta5d6ff"Ta5d6ffsending ? Ta5d6ff'Ta5d6ffmdi-loadingTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffmdi-sendTa5d6ff'Ta5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff{ Ta5d6ff'Ta5d6ffanimate-spinTa5d6ff'Ta5d6ff: sending }Ta5d6ff"
Te6edf3size=Ta5d6ff"Ta5d6ff14Ta5d6ff"
Tff7b72>Tff7b72</v-icon>
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</template>
Tff7b72<scriptTff7b72>
export default {
name: "MiniChat",
props: {
destinationHash: {
type: String,
required: true,
},
},
data() {
return {
messages: [],
newMessage: "",
loading: false,
sending: false,
};
},
watch: {
destinationHash: {
immediate: true,
handler() {
this.fetchMessages();
},
},
},
mounted() {
// Listen for new messages via websocket if possible
// For now we'll just poll or rely on parent updates if needed
},
methods: {
async fetchMessages() {
if (!this.destinationHash) return;
this.loading = true;
try {
const response = await window.api.get(
`/api/v1/lxmf-messages/conversation/${this.destinationHash}?count=20&order=desc`
);
this.messages = (response.data.lxmf_messages || []).reverse();
this.scrollToBottom();
} catch (e) {
console.error("Failed to fetch messages", e);
} finally {
this.loading = false;
}
},
async sendMessage() {
if (!this.newMessage.trim() || this.sending) return;
this.sending = true;
try {
const response = await window.api.post("/api/v1/lxmf-messages/send", {
lxmf_message: {
destination_hash: this.destinationHash,
content: this.newMessage,
},
});
// Add message to list locally for immediate feedback
const msg = response.data.lxmf_message;
this.messages.push({
hash: msg.hash,
content: msg.content,
is_outbound: true,
timestamp: msg.created_at,
});
this.newMessage = "";
this.scrollToBottom();
} catch (e) {
console.error("Failed to send message", e);
} finally {
this.sending = false;
}
},
scrollToBottom() {
this.$nextTick(() => {
if (this.$refs.messageList) {
this.$refs.messageList.scrollTop = this.$refs.messageList.scrollHeight;
}
});
},
formatTime(ts) {
if (!ts) return "";
const date = new Date(ts * 1000);
if (isNaN(date.getTime())) return "";
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
},
},
};
Tff7b72</script>
Served by rngit 1.5.2 - Generated in 0.1s